// Copyright 2017-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "MDCTextInputControllerBase.h"
#import "private/MDCTextInputControllerBase+Subclassing.h"

#import "MDCMultilineTextField.h"
#import "MDCTextField.h"
#import "MDCTextInput.h"
#import "MDCTextInputBorderView.h"
#import "MDCTextInputCharacterCounter.h"
#import "MDCTextInputController.h"
#import "MDCTextInputControllerFloatingPlaceholder.h"
#import "MDCTextInputUnderlineView.h"

#import "MaterialAnimationTiming.h"
#import "MaterialMath.h"
#import "MaterialPalettes.h"
#import "MaterialTypography.h"

#pragma mark - Constants

const CGFloat MDCTextInputControllerBaseDefaultBorderRadius = 4;
static const CGFloat MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault =
    (CGFloat)0.75;
static const CGFloat MDCTextInputControllerBaseDefaultHintTextOpacity = (CGFloat)0.54;
static const CGFloat MDCTextInputControllerBaseDefaultPadding = 8;

static const NSTimeInterval
    MDCTextInputControllerBaseDefaultFloatingPlaceholderDownAnimationDuration = (CGFloat)0.266666;
static const NSTimeInterval
    MDCTextInputControllerBaseDefaultFloatingPlaceholderUpAnimationDuration = (CGFloat)0.3;
static const NSTimeInterval kDefaultErrorAnnouncementDelay = (CGFloat)0.050;

static inline UIColor *MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault() {
  return [UIColor colorWithWhite:0 alpha:MDCTextInputControllerBaseDefaultHintTextOpacity];
}

static inline UIColor *MDCTextInputControllerBaseDefaultActiveColorDefault() {
  return [MDCPalette bluePalette].accent700;
}

static inline UIColor *MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault() {
  return [UIColor lightGrayColor];
}

static inline UIColor *MDCTextInputControllerBaseDefaultTextErrorColorDefault() {
  return [MDCPalette redPalette].accent400;
}

#pragma mark - Class Properties

static BOOL _floatingEnabledDefault = YES;
static BOOL _mdc_adjustsFontForContentSizeCategoryDefault = NO;

static CGFloat _floatingPlaceholderScaleDefault =
    MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault;
static CGFloat _underlineHeightActiveDefault = 0;
static CGFloat _underlineHeightNormalDefault = 0;

static UIColor *_activeColorDefault;
static UIColor *_borderFillColorDefault;
static UIColor *_disabledColorDefault;
static UIColor *_errorColorDefault;
static UIColor *_floatingPlaceholderActiveColorDefault;
static UIColor *_floatingPlaceholderNormalColorDefault;
static UIColor *_inlinePlaceholderColorDefault;
static UIColor *_leadingUnderlineLabelTextColorDefault;
static UIColor *_normalColorDefault;
static UIColor *_trailingUnderlineLabelTextColorDefault;

static UIFont *_inlinePlaceholderFontDefault;
static UIFont *_leadingUnderlineLabelFontDefault;
static UIFont *_textInputFontDefault;
static UIColor *_textInputClearButtonTintColorDefault;
static UIFont *_trailingUnderlineLabelFontDefault;

static UIRectCorner _roundedCornersDefault = 0;

static UITextFieldViewMode _underlineViewModeDefault = UITextFieldViewModeWhileEditing;

@interface MDCTextInputControllerBase () {
  BOOL _mdc_adjustsFontForContentSizeCategory;

  MDCTextInputAllCharactersCounter *_characterCounter;

  NSNumber *_floatingPlaceholderScale;

  UIColor *_activeColor;
  UIColor *_borderFillColor;
  UIColor *_borderStrokeColor;
  UIColor *_disabledColor;
  UIColor *_errorColor;
  UIColor *_floatingPlaceholderActiveColor;
  UIColor *_floatingPlaceholderNormalColor;
  UIColor *_floatingPlaceholderErrorActiveColor;
  UIColor *_inlinePlaceholderColor;
  UIColor *_leadingUnderlineLabelTextColor;
  UIColor *_normalColor;
  UIColor *_trailingUnderlineLabelTextColor;

  UIFont *_inlinePlaceholderFont;
  UIFont *_leadingUnderlineLabelFont;
  UIFont *_textInputFont;
  UIColor *_textInputClearButtonTintColor;
  UIFont *_trailingUnderlineLabelFont;

  UIRectCorner _roundedCorners;
}

@property(nonatomic, strong) MDCTextInputAllCharactersCounter *internalCharacterCounter;

@property(nonatomic, copy) NSArray<NSLayoutConstraint *> *placeholderAnimationConstraints;

@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintLeading;
@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintTop;
@property(nonatomic, strong) NSLayoutConstraint *placeholderAnimationConstraintTrailing;

@property(nonatomic, copy) NSString *errorAccessibilityValue;
@property(nonatomic, copy, readwrite) NSString *errorText;
@property(nonatomic, copy) NSString *helperAccessibilityLabel;
@property(nonatomic, copy) NSString *previousLeadingText;

@end

@implementation MDCTextInputControllerBase

@synthesize characterCountMax = _characterCountMax;
@synthesize characterCountViewMode = _characterCountViewMode;
@synthesize floatingEnabled = _floatingEnabled;
@synthesize roundedCorners = _roundedCorners;
@synthesize textInput = _textInput;
@synthesize underlineHeightActive = _underlineHeightActive;
@synthesize underlineHeightNormal = _underlineHeightNormal;
@synthesize underlineViewMode = _underlineViewMode;

// TODO: (larche): Support in-line auto complete.

- (instancetype)init {
  self = [super init];
  if (self) {
    if ([self isMemberOfClass:[MDCTextInputControllerBase class]]) {
      NSLog(@"NOTE: MDCTextInputControllerBase is a base class meant to be subclassed.\nFor more"
             "information, see the README.md file");
    }
    [self commonMDCTextInputControllerBaseInitialization];
  }

  return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
  self = [super init];
  if (self) {
    [self commonMDCTextInputControllerBaseInitialization];

    // This should happen last because it relies on the state of a ton of properties.
    [self setupInput];
  }
  return self;
}

- (instancetype)initWithTextInput:(UIView<MDCTextInput> *)textInput {
  self = [self init];
  if (self) {
    _textInput = textInput;

    // This should happen last because it relies on the state of a ton of properties.
    [self setupInput];
  }

  return self;
}

- (instancetype)copyWithZone:(__unused NSZone *)zone {
  MDCTextInputControllerBase *copy = [[[self class] alloc] init];

  copy.activeColor = self.activeColor;
  copy.borderFillColor = self.borderFillColor;
  copy.borderStrokeColor = self.borderStrokeColor;
  copy.characterCounter = self.characterCounter;  // Just a pointer value copy
  copy.characterCountViewMode = self.characterCountViewMode;
  copy.characterCountMax = self.characterCountMax;
  copy.roundedCorners = self.roundedCorners;
  copy.disabledColor = self.disabledColor;
  copy.errorAccessibilityValue = [self.errorAccessibilityValue copy];
  copy.errorColor = self.errorColor;
  copy.errorText = [self.errorText copy];
  copy.expandsOnOverflow = self.expandsOnOverflow;
  copy.floatingEnabled = self.isFloatingEnabled;
  copy.floatingPlaceholderActiveColor = self.floatingPlaceholderActiveColor;
  copy.floatingPlaceholderNormalColor = self.floatingPlaceholderNormalColor;
  copy.floatingPlaceholderErrorActiveColor = self.floatingPlaceholderErrorActiveColor;
  copy.floatingPlaceholderScale = self.floatingPlaceholderScale;
  copy.helperText = [self.helperText copy];
  copy.inlinePlaceholderColor = self.inlinePlaceholderColor;
  copy.inlinePlaceholderFont = self.inlinePlaceholderFont;
  copy.leadingUnderlineLabelFont = self.leadingUnderlineLabelFont;
  copy.leadingUnderlineLabelTextColor = self.leadingUnderlineLabelTextColor;
  copy.minimumLines = self.minimumLines;
  copy.normalColor = self.normalColor;
  copy.previousLeadingText = [self.previousLeadingText copy];
  copy.textInput = self.textInput;  // Just a pointer value copy
  copy.trailingUnderlineLabelFont = self.trailingUnderlineLabelFont;
  copy.trailingUnderlineLabelTextColor = self.trailingUnderlineLabelTextColor;
  copy.underlineHeightActive = self.underlineHeightActive;
  copy.underlineHeightNormal = self.underlineHeightNormal;
  copy.underlineViewMode = self.underlineViewMode;

  return copy;
}

- (void)dealloc {
  [self unsubscribeFromNotifications];
}

- (void)commonMDCTextInputControllerBaseInitialization {
  _roundedCorners = [self class].roundedCornersDefault;
  _characterCountViewMode = UITextFieldViewModeAlways;
  _disabledColor = [self class].disabledColorDefault;
  _expandsOnOverflow = YES;
  _floatingEnabled = [self class].isFloatingEnabledDefault;
  _internalCharacterCounter = [[MDCTextInputAllCharactersCounter alloc] init];
  _minimumLines = 1;
  _underlineHeightActive = [self class].underlineHeightActiveDefault;
  _underlineHeightNormal = [self class].underlineHeightNormalDefault;
  _underlineViewMode = [self class].underlineViewModeDefault;
  _textInput.hidesPlaceholderOnInput = NO;

  [self forceUpdatePlaceholderY];
}

- (void)setupInput {
  if (!_textInput) {
    return;
  }

  // This controller will handle Dynamic Type and all fonts for the text input
  _mdc_adjustsFontForContentSizeCategory =
      _textInput.mdc_adjustsFontForContentSizeCategory ||
      [self class].mdc_adjustsFontForContentSizeCategoryDefault;
  _textInput.underline.disabledColor = self.disabledColor;
  _textInput.mdc_adjustsFontForContentSizeCategory = NO;
  _textInput.positioningDelegate = self;
  _textInput.hidesPlaceholderOnInput = !self.isFloatingEnabled;
  _textInput.textInsetsMode = [self textInsetModeDefault];

  if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
      [_textInput respondsToSelector:@selector(setMinimumLines:)]) {
    ((id<MDCMultilineTextInput>)_textInput).minimumLines = self.minimumLines;
  }

  if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
      [_textInput respondsToSelector:@selector(setExpandsOnOverflow:)]) {
    ((id<MDCMultilineTextInput>)_textInput).expandsOnOverflow = self.expandsOnOverflow;
  }

  [self subscribeForNotifications];
  _textInput.underline.color = [self class].normalColorDefault;
  _textInput.clearButton.tintColor = self.textInputClearButtonTintColor;
  [self forceUpdatePlaceholderY];
}

- (MDCTextInputTextInsetsMode)textInsetModeDefault {
  return MDCTextInputTextInsetsModeAlways;
}

- (void)subscribeForNotifications {
  if (!_textInput) {
    return;
  }
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];

  [defaultCenter addObserver:self
                    selector:@selector(textInputDidToggleEnabled:)
                        name:MDCTextInputDidToggleEnabledNotification
                      object:_textInput];

  if ([_textInput isKindOfClass:[UITextField class]]) {
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidBeginEditing:)
                          name:UITextFieldTextDidBeginEditingNotification
                        object:_textInput];
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidChange:)
                          name:UITextFieldTextDidChangeNotification
                        object:_textInput];
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidEndEditing:)
                          name:UITextFieldTextDidEndEditingNotification
                        object:_textInput];
  }

  if ([_textInput isKindOfClass:[MDCMultilineTextField class]]) {
    MDCMultilineTextField *textField = (MDCMultilineTextField *)_textInput;
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidBeginEditing:)
                          name:UITextViewTextDidBeginEditingNotification
                        object:textField.textView];
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidChange:)
                          name:UITextViewTextDidChangeNotification
                        object:textField.textView];
    [defaultCenter addObserver:self
                      selector:@selector(textInputDidEndEditing:)
                          name:UITextViewTextDidEndEditingNotification
                        object:textField.textView];
  }

  [defaultCenter addObserver:self
                    selector:@selector(textInputDidChange:)
                        name:MDCTextFieldTextDidSetTextNotification
                      object:_textInput];
}

- (void)unsubscribeFromNotifications {
  NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter removeObserver:self];
}

#pragma mark - Border Customization

- (void)updateBorder {
  self.textInput.borderView.borderFillColor = self.borderFillColor;
  self.textInput.borderView.borderStrokeColor = self.borderStrokeColor;
  self.textInput.borderPath = [self defaultBorderPath];
}

- (UIBezierPath *)defaultBorderPath {
  CGRect borderBound = self.textInput.bounds;
  borderBound.size.height = CGRectGetMaxY(self.textInput.underline.frame);
  return [UIBezierPath
      bezierPathWithRoundedRect:borderBound
              byRoundingCorners:self.roundedCorners
                    cornerRadii:CGSizeMake(MDCTextInputControllerBaseDefaultBorderRadius,
                                           MDCTextInputControllerBaseDefaultBorderRadius)];
}

#pragma mark - Character Max Implementation

- (NSUInteger)characterCount {
  return [self.characterCounter characterCountForTextInput:self.textInput];
}

- (id<MDCTextInputCharacterCounter>)characterCounter {
  if (!_characterCounter) {
    _characterCounter = self.internalCharacterCounter;
  }
  return _characterCounter;
}

- (void)setCharacterCounter:(id<MDCTextInputCharacterCounter>)characterCounter {
  if (_characterCounter != characterCounter) {
    _characterCounter = characterCounter;
    [self updateLayout];
  }
}

- (void)setCharacterCountMax:(NSUInteger)characterCountMax {
  if (_characterCountMax != characterCountMax) {
    _characterCountMax = characterCountMax;
    [self updateLayout];
  }
}

#pragma mark - Cursor Customization

- (void)updateCursor {
  self.textInput.cursorColor = (self.isDisplayingErrorText || self.isDisplayingCharacterCountError)
                                   ? self.errorColor
                                   : self.activeColor;
}

#pragma mark - Leading Label Customization

- (void)updateLeadingUnderlineLabel {
  UIFont *font = self.leadingUnderlineLabelFont;
  if (self.mdc_adjustsFontForContentSizeCategory) {
    // TODO: (#4331) This needs to be converted to the new text scheme.
    font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleCaption
                              scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
  }
  self.textInput.leadingUnderlineLabel.font = font;

  self.textInput.leadingUnderlineLabel.textColor =
      (self.isDisplayingErrorText || self.isDisplayingCharacterCountError)
          ? self.errorColor
          : self.leadingUnderlineLabelTextColor;
}

#pragma mark - TextInput Customization

- (void)updateTextInput {
  UIFont *font = self.textInputFont;
  if (self.mdc_adjustsFontForContentSizeCategory) {
    font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
                              scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
    // TODO: (#4331) This needs to be converted to the new text scheme.
  }
  self.textInput.font = font;
}

#pragma mark - Placeholder Customization

// This updates the placeholder's visual characteristics and not its layout. See the section
// "Placeholder Floating" for methods that update the layout.
- (void)updatePlaceholder {
  UIFont *placeHolderFont = self.inlinePlaceholderFont;
  if (self.mdc_adjustsFontForContentSizeCategory) {
    // TODO: (#4331) This needs to be converted to the new text scheme.
    placeHolderFont =
        [placeHolderFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleBody1
                                      scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
  }

  // Aside from not wanting to kick off extra work for setting a font that hasn't changed, we use
  // this custom equality check to prevent an edge case that caused a 1 pixel change in width even
  // when the important parts of the new font were the same as the existing font.
  if (![self.textInput.placeholderLabel.font mdc_isSimplyEqual:placeHolderFont]) {
    self.textInput.placeholderLabel.font = placeHolderFont;
  }

  UIColor *placeholderColor;
  if ([self isPlaceholderUp]) {
    UIColor *errorColor = self.textInput.isEditing
                              ? (self.floatingPlaceholderErrorActiveColor ?: self.errorColor)
                              : self.errorColor;
    UIColor *nonErrorColor = self.textInput.isEditing ? self.floatingPlaceholderActiveColor
                                                      : self.floatingPlaceholderNormalColor;
    placeholderColor = (self.isDisplayingCharacterCountError || self.isDisplayingErrorText)
                           ? errorColor
                           : nonErrorColor;
  } else {
    placeholderColor = self.inlinePlaceholderColor;
  }
  if (!self.textInput.isEnabled) {
    placeholderColor = self.disabledColor;
  }
  self.textInput.placeholderLabel.textColor = placeholderColor;
}

#pragma mark - Placeholder Floating

// 'Up' in these methods always means the placeholder is floating; it's .y is smaller and it's not
// 'inline' with the text input. The placeholder also doesn't disappear when you type since it's
// functioning as a title label when 'up'.

- (void)movePlaceholderToUp:(BOOL)isToUp {
  if ([self isPlaceholderUp] == isToUp) {
    return;
  }

  CGFloat scaleFactor = (CGFloat)self.floatingPlaceholderScale.floatValue;
  CGAffineTransform floatingPlaceholderScaleTransform =
      CGAffineTransformMakeScale(scaleFactor, scaleFactor);

  // The animation is accomplished pretty simply. A constraint for vertical and a constraint for
  // horizontal offset, both with a required priority (1000), are acivated on the placeholderLabel.
  // A simple scale transform is also applied. Then it's animated through the UIView animation API
  // (layoutIfNeeded). If in reverse (isToUp == NO), these things are just removed / deactivated.

  CGAffineTransform scaleTransform =
      isToUp ? floatingPlaceholderScaleTransform : CGAffineTransformIdentity;

  // We do this beforehand to flush the layout engine.
  [self.textInput layoutIfNeeded];
  [self updatePlaceholderAnimationConstraints:isToUp];
  [UIView animateWithDuration:[CATransaction animationDuration]
      animations:^{
        self.textInput.placeholderLabel.transform = scaleTransform;
        [self updatePlaceholder];
        [self updateBorder];
        [self.textInput layoutIfNeeded];
      }
      completion:^(__unused BOOL finished) {
        if (!isToUp) {
          [self cleanupPlaceholderAnimationConstraints];
        }
      }];
}

- (BOOL)isPlaceholderUp {
  return self.placeholderAnimationConstraints.count > 0 &&
         !CGAffineTransformEqualToTransform(self.textInput.placeholderLabel.transform,
                                            CGAffineTransformIdentity);
}

// Sometimes we set up the animation constraints for floating up before we have a width for the text
// field. Since there is math that needs to compensate for the transform in relation to the width,
// we update the constants on the constraints.
//
// Note: this method is not called inside updateLayout.
- (void)updatePlaceholderAnimationConstraints:(BOOL)isToUp {
  if (isToUp) {
    UIOffset offset = [self floatingPlaceholderOffset];

    // Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
    // See MDCTextInputController.h.
    UIEdgeInsets insets = self.textInput.textInsets;

    CGFloat leadingConstant = [self floatingPlaceholderAnimationConstraintLeadingConstant:insets
                                                                                   offset:offset];
    if (!self.placeholderAnimationConstraintLeading) {
      self.placeholderAnimationConstraintLeading =
          [NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
                                       attribute:NSLayoutAttributeLeading
                                       relatedBy:NSLayoutRelationEqual
                                          toItem:self.textInput
                                       attribute:NSLayoutAttributeLeading
                                      multiplier:1
                                        constant:leadingConstant];
    }
    self.placeholderAnimationConstraintLeading.constant = leadingConstant;

    if (!self.placeholderAnimationConstraintTop) {
      self.placeholderAnimationConstraintTop =
          [NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
                                       attribute:NSLayoutAttributeTop
                                       relatedBy:NSLayoutRelationEqual
                                          toItem:self.textInput
                                       attribute:NSLayoutAttributeTop
                                      multiplier:1
                                        constant:offset.vertical];
    }
    self.placeholderAnimationConstraintTop.constant = offset.vertical;

    CGFloat trailingConstant = [self floatingPlaceholderAnimationConstraintTrailingConstant:insets
                                                                                     offset:offset];
    if (!self.placeholderAnimationConstraintTrailing) {
      self.placeholderAnimationConstraintTrailing =
          [NSLayoutConstraint constraintWithItem:self.textInput.placeholderLabel
                                       attribute:NSLayoutAttributeTrailing
                                       relatedBy:NSLayoutRelationEqual
                                          toItem:self.textInput
                                       attribute:NSLayoutAttributeTrailing
                                      multiplier:1
                                        constant:trailingConstant];
      self.placeholderAnimationConstraintTrailing.priority = UILayoutPriorityDefaultHigh - 1;
    }
    self.placeholderAnimationConstraintTrailing.constant = trailingConstant;

    self.placeholderAnimationConstraints = @[
      self.placeholderAnimationConstraintLeading, self.placeholderAnimationConstraintTop,
      self.placeholderAnimationConstraintTrailing
    ];
    [NSLayoutConstraint activateConstraints:self.placeholderAnimationConstraints];
  } else {
    [NSLayoutConstraint deactivateConstraints:self.placeholderAnimationConstraints];
  }
}

// Sometimes we set up the animation constraints for floating up before we have a width for the text
// field. Since there is math that needs to compensate for the transform in relation to the width,
// we check to see if the constraints still have the correct constants.
- (BOOL)needsUpdatePlaceholderAnimationConstraintsToUp {
  if (![self isPlaceholderUp]) {
    return NO;
  }

  // Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
  // See MDCTextInputController.h.
  UIEdgeInsets insets = self.textInput.textInsets;

  UIOffset offset = [self floatingPlaceholderOffset];

  CGFloat leadingConstant = [self floatingPlaceholderAnimationConstraintLeadingConstant:insets
                                                                                 offset:offset];
  CGFloat trailingConstant = [self floatingPlaceholderAnimationConstraintTrailingConstant:insets
                                                                                   offset:offset];

  return self.placeholderAnimationConstraintLeading.constant != leadingConstant &&
         self.placeholderAnimationConstraintTrailing.constant != trailingConstant;
}

// When placeholder is not up, it just uses the layout code that comes for free from the text field
// itself. That means these constraints need go away. So, we can use the existence of them as a
// way to check if the placeholder is floating up. See isplaceholderUp.
- (void)cleanupPlaceholderAnimationConstraints {
  // We need to clear only deactivated constraints and to prevent the clearing of active
  // constraints.
  if (self.placeholderAnimationConstraintLeading.active &&
      self.placeholderAnimationConstraintTop.active &&
      self.placeholderAnimationConstraintTrailing.active) {
    return;
  }
  self.placeholderAnimationConstraints = nil;
  self.placeholderAnimationConstraintLeading = nil;
  self.placeholderAnimationConstraintTop = nil;
  self.placeholderAnimationConstraintTrailing = nil;
}

// Sometimes the text field is not showing the correct layout for its values (like when it's created
// with .text already entered) so we make sure it's in the right place always.
//
// Note that this calls updateLayout inside it so it is not included in updateLayout.
- (void)forceUpdatePlaceholderY {
  BOOL isDirectionToUp = NO;
  if (self.floatingEnabled) {
    isDirectionToUp = self.textInput.hasTextContent || self.textInput.isEditing;
  }

  [CATransaction begin];
  [CATransaction setDisableActions:YES];
  [self movePlaceholderToUp:isDirectionToUp];
  [self updateLayout];

  [CATransaction commit];

  self.textInput.hidesPlaceholderOnInput = !self.floatingEnabled;
  [self.textInput layoutIfNeeded];
}

// The placeholder has a simple scale transform on it. The default is to reduce the size by 25%.
// Unfortunately, auto layout has no concept of compensating for transforms and it keeps the same
// alignmentRect as if it weren't transformed at all. That gives the impression the placeholder,
// when floating up, is too far to the trailing side. This offset is part of a compensation for
// that. It calculates the amount of space between the alignmentRect's sides and the actual shrunken
// placeholder label.
- (UIOffset)floatingPlaceholderOffset {
  CGFloat vertical = MDCTextInputControllerBaseDefaultPadding;

  // Offsets needed due to transform working on normal (0.5,0.5) anchor point.
  // Why no anchor point of (0,0)? Because autolayout doesn't play well with anchor points.
  vertical -= self.textInput.placeholderLabel.font.lineHeight *
              (1 - (CGFloat)self.floatingPlaceholderScale.floatValue) * (CGFloat)0.5;

  // Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
  // See MDCTextInputController.h.
  UIEdgeInsets insets = self.textInput.textInsets;

  CGFloat placeholderMaxWidth =
      CGRectGetWidth(self.textInput.bounds) / self.floatingPlaceholderScale.floatValue -
      insets.left - insets.right;

  CGFloat placeholderWidth =
      [self.textInput.placeholderLabel systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]
          .width;
  if (placeholderWidth > placeholderMaxWidth) {
    placeholderWidth = placeholderMaxWidth;
  }

  CGFloat horizontal =
      placeholderWidth * (1 - (CGFloat)self.floatingPlaceholderScale.floatValue) * (CGFloat)0.5;

  return UIOffsetMake(horizontal, vertical);
}

// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
- (CGFloat)floatingPlaceholderAnimationConstraintLeadingConstant:(UIEdgeInsets)textInsets
                                                          offset:(UIOffset)offset {
  CGFloat constant = textInsets.left - offset.horizontal;
  return constant;
}

// Remember, the insets are always in LTR. It's automatically flipped when used in RTL.
// See MDCTextInputController.h.
- (CGFloat)floatingPlaceholderAnimationConstraintTrailingConstant:(UIEdgeInsets)textInsets
                                                           offset:(UIOffset)offset {
  CGFloat constant = offset.horizontal - textInsets.right;
  return constant;
}

#pragma mark - Trailing Label Customization

- (void)updateTrailingUnderlineLabel {
  if (!self.characterCountMax) {
    self.textInput.trailingUnderlineLabel.text = nil;
  } else {
    self.textInput.trailingUnderlineLabel.text = [self characterCountText];
    UIFont *font = self.trailingUnderlineLabelFont;
    if (self.mdc_adjustsFontForContentSizeCategory) {
      // TODO: (#4331) This needs to be converted to the new text scheme.
      font = [font mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleCaption
                                scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory];
    }
    self.textInput.trailingUnderlineLabel.font = font;
  }

  UIColor *textColor = self.trailingUnderlineLabelTextColor;

  if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
    textColor = self.errorColor;
  }

  switch (self.characterCountViewMode) {
    case UITextFieldViewModeAlways:
      break;
    case UITextFieldViewModeWhileEditing:
      textColor = !self.textInput.isEditing ? [UIColor clearColor] : textColor;
      break;
    case UITextFieldViewModeUnlessEditing:
      textColor = self.textInput.isEditing ? [UIColor clearColor] : textColor;
      break;
    case UITextFieldViewModeNever:
      textColor = [UIColor clearColor];
      break;
  }

  self.textInput.trailingUnderlineLabel.textColor = textColor;
}

- (NSString *)characterCountText {
  // TODO: (larche) Localize
  return [NSString stringWithFormat:@"%lu / %lu", (unsigned long)[self characterCount],
                                    (unsigned long)self.characterCountMax];
}

#pragma mark - Underline Customization

- (void)updateUnderline {
  UIColor *underlineColor;
  CGFloat underlineHeight;

  switch (self.underlineViewMode) {
    case UITextFieldViewModeAlways:
      underlineColor = self.activeColor;
      underlineHeight = self.underlineHeightActive;
      break;
    case UITextFieldViewModeWhileEditing:
      underlineColor = self.textInput.isEditing ? self.activeColor : self.normalColor;
      underlineHeight =
          self.textInput.isEditing ? self.underlineHeightActive : self.underlineHeightNormal;
      break;
    case UITextFieldViewModeUnlessEditing:
      underlineColor = !self.textInput.isEditing ? self.activeColor : self.normalColor;
      underlineHeight =
          !self.textInput.isEditing ? self.underlineHeightActive : self.underlineHeightNormal;
      break;
    case UITextFieldViewModeNever:
    default:
      underlineColor = self.normalColor;
      underlineHeight = self.underlineHeightNormal;
      break;
  }
  self.textInput.underline.color =
      (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) ? self.errorColor
                                                                           : underlineColor;
  self.textInput.underline.lineHeight = underlineHeight;

  self.textInput.underline.disabledColor = self.disabledColor;
}

#pragma mark - Underline Labels Fonts

+ (UIFont *)placeholderFont {
  return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleBody1];
  // TODO: (#4331) This needs to be converted to the new text scheme.
}

+ (UIFont *)underlineLabelsFont {
  // TODO: (#4331) This needs to be converted to the new text scheme.
  return [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleCaption];
}

#pragma mark - Properties Implementation

- (UIColor *)activeColor {
  return _activeColor ? _activeColor : [self class].activeColorDefault;
}

- (void)setActiveColor:(UIColor *)activeColor {
  if (![_activeColor isEqual:activeColor]) {
    _activeColor = activeColor;
    [self updateLayout];
  }
}

+ (UIColor *)activeColorDefault {
  if (!_activeColorDefault) {
    _activeColorDefault = MDCTextInputControllerBaseDefaultActiveColorDefault();
  }
  return _activeColorDefault;
}

+ (void)setActiveColorDefault:(UIColor *)activeColorDefault {
  _activeColorDefault = activeColorDefault ? activeColorDefault
                                           : MDCTextInputControllerBaseDefaultActiveColorDefault();
}

- (UIColor *)borderFillColor {
  if (!_borderFillColor) {
    _borderFillColor = [self class].borderFillColorDefault;
  }
  return _borderFillColor;
}

- (void)setBorderFillColor:(UIColor *)borderFillColor {
  if (_borderFillColor != borderFillColor) {
    _borderFillColor = borderFillColor ? borderFillColor : [self class].borderFillColorDefault;
    [self updateBorder];
  }
}

+ (UIColor *)borderFillColorDefault {
  if (!_borderFillColorDefault) {
    _borderFillColorDefault = [UIColor clearColor];
  }
  return _borderFillColorDefault;
}

+ (void)setBorderFillColorDefault:(UIColor *)borderFillColorDefault {
  _borderFillColorDefault = borderFillColorDefault ? borderFillColorDefault : [UIColor clearColor];
}

- (void)setCharacterCountViewMode:(UITextFieldViewMode)characterCountViewMode {
  if (_characterCountViewMode != characterCountViewMode) {
    _characterCountViewMode = characterCountViewMode;

    [self updateLayout];
  }
}

- (UIColor *)borderStrokeColor {
  return _borderStrokeColor;
}

- (void)setBorderStrokeColor:(UIColor *)borderStrokeColor {
  if (_borderStrokeColor != borderStrokeColor) {
    _borderStrokeColor = borderStrokeColor;
    [self updateBorder];
  }
}

- (UIColor *)disabledColor {
  if (!_disabledColor) {
    _disabledColor = [self class].disabledColorDefault;
  }
  return _disabledColor;
}

- (void)setDisabledColor:(UIColor *)disabledColor {
  if (_disabledColor != disabledColor) {
    _disabledColor = disabledColor ? disabledColor : [self class].disabledColorDefault;
    [self updateLayout];
  }
}

+ (UIColor *)disabledColorDefault {
  if (!_disabledColorDefault) {
    _disabledColorDefault = MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
  }
  return _disabledColorDefault;
}

+ (void)setDisabledColorDefault:(UIColor *)disabledColorDefault {
  _disabledColorDefault = disabledColorDefault
                              ? disabledColorDefault
                              : MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}

// This is when the character count text is red because the inputted text has more characters than
// the characterCountMax allows.
- (BOOL)isDisplayingCharacterCountError {
  return self.characterCountMax && [self characterCount] > self.characterCountMax;
}

- (BOOL)isDisplayingErrorText {
  return self.errorText != nil;
}

- (void)setErrorAccessibilityValue:(NSString *)errorAccessibilityValue {
  _errorAccessibilityValue = [errorAccessibilityValue copy];
}

- (void)setHelperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
  _helperAccessibilityLabel = [helperAccessibilityLabel copy];
  if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
    self.textInput.leadingUnderlineLabel.accessibilityLabel = _helperAccessibilityLabel;
  }
}

- (UIColor *)errorColor {
  if (!_errorColor) {
    _errorColor = [self class].errorColorDefault;
  }
  return _errorColor;
}

- (void)setErrorColor:(UIColor *)errorColor {
  if (![_errorColor isEqual:errorColor]) {
    _errorColor = errorColor ? errorColor : [self class].errorColorDefault;
    if (self.isDisplayingCharacterCountError || self.isDisplayingErrorText) {
      [self updateLeadingUnderlineLabel];
      [self updatePlaceholder];
      [self updateTrailingUnderlineLabel];
      [self updateUnderline];
    }
  }
}

+ (UIColor *)errorColorDefault {
  if (!_errorColorDefault) {
    _errorColorDefault = MDCTextInputControllerBaseDefaultTextErrorColorDefault();
  }
  return _errorColorDefault;
}

+ (void)setErrorColorDefault:(UIColor *)errorColorDefault {
  _errorColorDefault = errorColorDefault ? errorColorDefault
                                         : MDCTextInputControllerBaseDefaultTextErrorColorDefault();
}

- (void)setErrorText:(NSString *)errorText {
  _errorText = [errorText copy];
}

- (void)setExpandsOnOverflow:(BOOL)expandsOnOverflow {
  if (_expandsOnOverflow != expandsOnOverflow) {
    _expandsOnOverflow = expandsOnOverflow;
    if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
        [_textInput respondsToSelector:@selector(setExpandsOnOverflow:)]) {
      ((id<MDCMultilineTextInput>)_textInput).expandsOnOverflow = expandsOnOverflow;
    }
  }
}
- (UIColor *)floatingPlaceholderActiveColor {
  return _floatingPlaceholderActiveColor ? _floatingPlaceholderActiveColor
                                         : [self class].floatingPlaceholderActiveColorDefault;
}

- (void)setFloatingPlaceholderActiveColor:(UIColor *)floatingPlaceholderActiveColor {
  if (![_floatingPlaceholderActiveColor isEqual:floatingPlaceholderActiveColor]) {
    _floatingPlaceholderActiveColor = floatingPlaceholderActiveColor;
    [self updatePlaceholder];
  }
}

+ (UIColor *)floatingPlaceholderActiveColorDefault {
  if (!_floatingPlaceholderActiveColorDefault) {
    _floatingPlaceholderActiveColorDefault = [self class].activeColorDefault;
  }
  return _floatingPlaceholderActiveColorDefault;
}

+ (void)setFloatingPlaceholderActiveColorDefault:(UIColor *)floatingPlaceholderActiveColorDefault {
  _floatingPlaceholderActiveColorDefault = floatingPlaceholderActiveColorDefault
                                               ? floatingPlaceholderActiveColorDefault
                                               : [self class].activeColorDefault;
}

- (UIColor *)floatingPlaceholderNormalColor {
  return _floatingPlaceholderNormalColor ? _floatingPlaceholderNormalColor
                                         : [self class].floatingPlaceholderNormalColorDefault;
}

- (void)setFloatingPlaceholderNormalColor:(UIColor *)floatingPlaceholderNormalColor {
  if (![_floatingPlaceholderNormalColor isEqual:floatingPlaceholderNormalColor]) {
    _floatingPlaceholderNormalColor = floatingPlaceholderNormalColor;
    [self updatePlaceholder];
  }
}

+ (UIColor *)floatingPlaceholderNormalColorDefault {
  if (!_floatingPlaceholderNormalColorDefault) {
    _floatingPlaceholderNormalColorDefault = [self class].inlinePlaceholderColorDefault;
  }
  return _floatingPlaceholderNormalColorDefault;
}

+ (void)setFloatingPlaceholderNormalColorDefault:(UIColor *)floatingPlaceholderNormalColorDefault {
  _floatingPlaceholderNormalColorDefault = floatingPlaceholderNormalColorDefault
                                               ? floatingPlaceholderNormalColorDefault
                                               : [self class].inlinePlaceholderColorDefault;
}

- (UIColor *)floatingPlaceholderErrorActiveColor {
  return _floatingPlaceholderErrorActiveColor;
}

- (void)setFloatingPlaceholderErrorActiveColor:(UIColor *)floatingPlaceholderErrorActiveColor {
  if (![_floatingPlaceholderErrorActiveColor isEqual:floatingPlaceholderErrorActiveColor]) {
    _floatingPlaceholderErrorActiveColor = floatingPlaceholderErrorActiveColor;
    [self updatePlaceholder];
  }
}

- (void)setFloatingEnabled:(BOOL)floatingEnabled {
  if (_floatingEnabled != floatingEnabled) {
    _floatingEnabled = floatingEnabled;
    [self forceUpdatePlaceholderY];
  }
}

+ (BOOL)isFloatingEnabledDefault {
  return _floatingEnabledDefault;
}

+ (void)setFloatingEnabledDefault:(BOOL)floatingEnabledDefault {
  _floatingEnabledDefault = floatingEnabledDefault;
}

- (NSNumber *)floatingPlaceholderScale {
  if (_floatingPlaceholderScale == nil) {
    _floatingPlaceholderScale =
        [NSNumber numberWithFloat:(float)[self class].floatingPlaceholderScaleDefault];
  }
  return _floatingPlaceholderScale;
}

- (void)setFloatingPlaceholderScale:(NSNumber *)floatingPlaceholderScale {
  if (![_floatingPlaceholderScale isEqualToNumber:floatingPlaceholderScale]) {
    _floatingPlaceholderScale =
        (floatingPlaceholderScale != nil)
            ? floatingPlaceholderScale
            : [NSNumber numberWithFloat:(float)[self class].floatingPlaceholderScaleDefault];

    [self updatePlaceholder];
    [self.textInput setNeedsUpdateConstraints];
  }
}

+ (CGFloat)floatingPlaceholderScaleDefault {
  return _floatingPlaceholderScaleDefault;
}

+ (void)setFloatingPlaceholderScaleDefault:(CGFloat)floatingPlaceholderScaleDefault {
  _floatingPlaceholderScaleDefault = floatingPlaceholderScaleDefault;
  if (floatingPlaceholderScaleDefault <= 0) {
    _floatingPlaceholderScaleDefault =
        MDCTextInputControllerBaseDefaultFloatingPlaceholderScaleDefault;
  }
}

- (void)setHelperText:(NSString *)helperText {
  if (self.isDisplayingErrorText) {
    self.previousLeadingText = helperText;
  } else {
    if (![self.textInput.leadingUnderlineLabel.text isEqualToString:helperText]) {
      self.textInput.leadingUnderlineLabel.text = helperText;
      [self updateLayout];
    }
  }
}

- (NSString *)helperText {
  if (self.isDisplayingErrorText) {
    return self.previousLeadingText;
  } else {
    return self.textInput.leadingUnderlineLabel.text;
  }
}

- (void)setInlinePlaceholderColor:(UIColor *)inlinePlaceholderColor {
  if (![_inlinePlaceholderColor isEqual:inlinePlaceholderColor]) {
    _inlinePlaceholderColor = inlinePlaceholderColor;
    [self updatePlaceholder];
  }
}

- (UIColor *)inlinePlaceholderColor {
  return _inlinePlaceholderColor ? _inlinePlaceholderColor
                                 : [self class].inlinePlaceholderColorDefault;
}

+ (UIColor *)inlinePlaceholderColorDefault {
  if (!_inlinePlaceholderColorDefault) {
    _inlinePlaceholderColorDefault =
        MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
  }
  return _inlinePlaceholderColorDefault;
}

+ (void)setInlinePlaceholderColorDefault:(UIColor *)inlinePlaceholderColorDefault {
  _inlinePlaceholderColorDefault =
      inlinePlaceholderColorDefault
          ? inlinePlaceholderColorDefault
          : MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}

- (UIFont *)inlinePlaceholderFont {
  return _inlinePlaceholderFont ?: [self class].inlinePlaceholderFontDefault;
}

- (void)setInlinePlaceholderFont:(UIFont *)inlinePlaceholderFont {
  if (![_inlinePlaceholderFont isEqual:inlinePlaceholderFont]) {
    _inlinePlaceholderFont = inlinePlaceholderFont;
    [self updateLayout];
  }
}

+ (UIFont *)inlinePlaceholderFontDefault {
  return _inlinePlaceholderFontDefault ?: [[self class] placeholderFont];
}

+ (void)setInlinePlaceholderFontDefault:(UIFont *)inlinePlaceholderFontDefault {
  _inlinePlaceholderFontDefault = inlinePlaceholderFontDefault;
}

- (UIFont *)leadingUnderlineLabelFont {
  return _leadingUnderlineLabelFont ?: [self class].leadingUnderlineLabelFontDefault;
}

- (void)setLeadingUnderlineLabelFont:(UIFont *)leadingUnderlineLabelFont {
  if (![_leadingUnderlineLabelFont isEqual:leadingUnderlineLabelFont]) {
    _leadingUnderlineLabelFont = leadingUnderlineLabelFont;
    [self updateLayout];
  }
}

+ (UIFont *)leadingUnderlineLabelFontDefault {
  return _leadingUnderlineLabelFontDefault ?: [[self class] underlineLabelsFont];
}

+ (void)setLeadingUnderlineLabelFontDefault:(UIFont *)leadingUnderlineLabelFontDefault {
  _leadingUnderlineLabelFontDefault = leadingUnderlineLabelFontDefault;
}

- (UIColor *)leadingUnderlineLabelTextColor {
  return _leadingUnderlineLabelTextColor ? _leadingUnderlineLabelTextColor
                                         : [self class].leadingUnderlineLabelTextColorDefault;
}

- (void)setLeadingUnderlineLabelTextColor:(UIColor *)leadingUnderlineLabelTextColor {
  if (_leadingUnderlineLabelTextColor != leadingUnderlineLabelTextColor) {
    _leadingUnderlineLabelTextColor = leadingUnderlineLabelTextColor
                                          ? leadingUnderlineLabelTextColor
                                          : [self class].leadingUnderlineLabelTextColorDefault;

    [self updateLeadingUnderlineLabel];
  }
}

+ (UIColor *)leadingUnderlineLabelTextColorDefault {
  if (!_leadingUnderlineLabelTextColorDefault) {
    _leadingUnderlineLabelTextColorDefault =
        MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
  }
  return _leadingUnderlineLabelTextColorDefault;
}

+ (void)setLeadingUnderlineLabelTextColorDefault:(UIColor *)leadingUnderlineLabelTextColorDefault {
  _leadingUnderlineLabelTextColorDefault =
      leadingUnderlineLabelTextColorDefault
          ? leadingUnderlineLabelTextColorDefault
          : MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}

- (void)setMinimumLines:(NSUInteger)minimumLines {
  if (_minimumLines != minimumLines) {
    _minimumLines = minimumLines;
    if ([_textInput conformsToProtocol:@protocol(MDCMultilineTextInput)] &&
        [_textInput respondsToSelector:@selector(setMinimumLines:)]) {
      ((id<MDCMultilineTextInput>)_textInput).minimumLines = minimumLines;
    }
  }
}

- (UIColor *)normalColor {
  return _normalColor ? _normalColor : [self class].normalColorDefault;
}

- (void)setNormalColor:(UIColor *)normalColor {
  if (![_normalColor isEqual:normalColor]) {
    _normalColor = normalColor;
    [self updateLayout];
  }
}

+ (UIColor *)normalColorDefault {
  if (!_normalColorDefault) {
    _normalColorDefault = MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
  }
  return _normalColorDefault;
}

+ (void)setNormalColorDefault:(UIColor *)normalColorDefault {
  _normalColorDefault = normalColorDefault
                            ? normalColorDefault
                            : MDCTextInputControllerBaseDefaultNormalUnderlineColorDefault();
}

- (NSString *)placeholderText {
  return _textInput.placeholder;
}

- (void)setPlaceholderText:(NSString *)placeholderText {
  if ([_textInput.placeholder isEqualToString:placeholderText]) {
    return;
  }
  _textInput.placeholder = [placeholderText copy];
  if (self.isFloatingEnabled && _textInput.hasTextContent) {
    [self updatePlaceholderAnimationConstraints:YES];
  }
}

- (void)setPreviousLeadingText:(NSString *)previousLeadingText {
  _previousLeadingText = [previousLeadingText copy];
}

- (void)setRoundedCorners:(UIRectCorner)roundedCorners {
  if (_roundedCorners != roundedCorners) {
    _roundedCorners = roundedCorners;

    [self updateLayout];
  }
}

+ (UIRectCorner)roundedCornersDefault {
  return _roundedCornersDefault;
}

+ (void)setRoundedCornersDefault:(UIRectCorner)roundedCornersDefault {
  _roundedCornersDefault = roundedCornersDefault;
}

- (void)setTextInput:(UIView<MDCTextInput> *)textInput {
  if (_textInput != textInput) {
    [self unsubscribeFromNotifications];

    _textInput = textInput;
    [self setupInput];
  }
}

- (UIFont *)textInputFont {
  if (_textInputFont) {
    return _textInputFont;
  }
  return [self class].textInputFontDefault ?: self.textInput.font;
}

- (void)setTextInputFont:(UIFont *)textInputFont {
  if (![_textInputFont isEqual:textInputFont]) {
    _textInputFont = textInputFont;
    [self updateLayout];
  }
}

+ (UIFont *)textInputFontDefault {
  return _textInputFontDefault;
}

+ (void)setTextInputFontDefault:(UIFont *)textInputFontDefault {
  _textInputFontDefault = textInputFontDefault;
}

- (UIColor *)textInputClearButtonTintColor {
  if (_textInputClearButtonTintColor) {
    return _textInputClearButtonTintColor;
  }
  return [self class].textInputClearButtonTintColorDefault ?: self.textInput.clearButton.tintColor;
}

- (void)setTextInputClearButtonTintColor:(UIColor *)textInputClearButtonTintColor {
  _textInputClearButtonTintColor = textInputClearButtonTintColor;
  _textInput.clearButton.tintColor = _textInputClearButtonTintColor;
}

+ (UIColor *)textInputClearButtonTintColorDefault {
  return _textInputClearButtonTintColorDefault;
}

+ (void)setTextInputClearButtonTintColorDefault:(UIColor *)textInputClearButtonTintColorDefault {
  _textInputClearButtonTintColorDefault = textInputClearButtonTintColorDefault;
}

- (UIFont *)trailingUnderlineLabelFont {
  return _trailingUnderlineLabelFont ?: [self class].trailingUnderlineLabelFontDefault;
}

- (void)setTrailingUnderlineLabelFont:(UIFont *)trailingUnderlineLabelFont {
  if (![_trailingUnderlineLabelFont isEqual:trailingUnderlineLabelFont]) {
    _trailingUnderlineLabelFont = trailingUnderlineLabelFont;
    [self updateLayout];
  }
}

+ (UIFont *)trailingUnderlineLabelFontDefault {
  return _trailingUnderlineLabelFontDefault ?: [[self class] underlineLabelsFont];
}

+ (void)setTrailingUnderlineLabelFontDefault:(UIFont *)trailingUnderlineLabelFontDefault {
  _trailingUnderlineLabelFontDefault = trailingUnderlineLabelFontDefault;
}

- (UIColor *)trailingUnderlineLabelTextColor {
  return _trailingUnderlineLabelTextColor ? _trailingUnderlineLabelTextColor
                                          : [self class].trailingUnderlineLabelTextColorDefault;
}

- (void)setTrailingUnderlineLabelTextColor:(UIColor *)trailingUnderlineLabelTextColor {
  if (_trailingUnderlineLabelTextColor != trailingUnderlineLabelTextColor) {
    _trailingUnderlineLabelTextColor = trailingUnderlineLabelTextColor
                                           ? trailingUnderlineLabelTextColor
                                           : [self class].trailingUnderlineLabelTextColorDefault;

    [self updateTrailingUnderlineLabel];
  }
}

+ (UIColor *)trailingUnderlineLabelTextColorDefault {
  if (!_trailingUnderlineLabelTextColorDefault) {
    _trailingUnderlineLabelTextColorDefault =
        MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
  }
  return _trailingUnderlineLabelTextColorDefault;
}

+ (void)setTrailingUnderlineLabelTextColorDefault:
    (UIColor *)trailingUnderlineLabelTextColorDefault {
  _trailingUnderlineLabelTextColorDefault =
      trailingUnderlineLabelTextColorDefault
          ? trailingUnderlineLabelTextColorDefault
          : MDCTextInputControllerBaseDefaultInlinePlaceholderTextColorDefault();
}

- (void)setUnderlineHeightActive:(CGFloat)underlineHeightActive {
  if (_underlineHeightActive != underlineHeightActive) {
    _underlineHeightActive = underlineHeightActive;
    [self updateLayout];
  }
}

+ (CGFloat)underlineHeightActiveDefault {
  return _underlineHeightActiveDefault;
}

+ (void)setUnderlineHeightActiveDefault:(CGFloat)underlineHeightActiveDefault {
  _underlineHeightActiveDefault = underlineHeightActiveDefault;
}

- (void)setUnderlineHeightNormal:(CGFloat)underlineHeightNormal {
  if (_underlineHeightNormal != underlineHeightNormal) {
    _underlineHeightNormal = underlineHeightNormal;
    [self updateLayout];
  }
}

+ (CGFloat)underlineHeightNormalDefault {
  return _underlineHeightNormalDefault;
}

+ (void)setUnderlineHeightNormalDefault:(CGFloat)underlineHeightNormalDefault {
  _underlineHeightNormalDefault = underlineHeightNormalDefault;
}

- (void)setUnderlineViewMode:(UITextFieldViewMode)underlineViewMode {
  if (_underlineViewMode != underlineViewMode) {
    _underlineViewMode = underlineViewMode;
    [self updateLayout];
  }
}

+ (UITextFieldViewMode)underlineViewModeDefault {
  return _underlineViewModeDefault;
}

+ (void)setUnderlineViewModeDefault:(UITextFieldViewMode)underlineViewModeDefault {
  _underlineViewModeDefault = underlineViewModeDefault;
}

#pragma mark - Layout

- (void)updateLayout {
  if (!_textInput) {
    return;
  }

  [self updateCursor];
  [self updatePlaceholder];
  [self updateLeadingUnderlineLabel];
  [self updateTrailingUnderlineLabel];
  [self updateTextInput];
  [self updateUnderline];
  [self updateBorder];
}

#pragma mark - MDCTextFieldPositioningDelegate

// clang-format off
/**
 textInsets: is the source of truth for vertical layout. It's used to figure out the proper
 height and also where to place the placeholder / text field.

 NOTE: It's applied before the textRect is flipped for RTL. So all calculations are done here à la
 LTR.

 The vertical layout is, at most complex, this form:
 MDCTextInputControllerBaseDefaultPadding                             // Top padding
 MDCRint(self.textInput.placeholderLabel.font.lineHeight * scale)     // Placeholder when up
 MDCTextInputControllerBaseDefaultPadding                             // Padding
 MDCCeil(MAX(self.textInput.font.lineHeight,                          // Text field or placeholder
             self.textInput.placeholderLabel.font.lineHeight))
 MDCTextInputControllerBaseDefaultPadding                             // Padding to underline
 --Underline--                                                        // Underline (height not counted)
 underlineLabelsOffset                                                // Depends on text insets mode
 */
// clang-format on
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets
    withSizeThatFitsWidthHint:(CGFloat)widthHint {
  // NOTE: UITextFields have a centerY based layout. And you can change EITHER the height or the Y.
  // Not both. Don't know why. So, we have to leave the text rect as big as the bounds and move it
  // to a Y that works. In other words, no bottom inset will make a difference here for UITextFields
  UIEdgeInsets textInsets = defaultInsets;

  if (self.isFloatingEnabled) {
    textInsets.top = MDCTextInputControllerBaseDefaultPadding +
                     MDCRint(self.textInput.placeholderLabel.font.lineHeight *
                             (CGFloat)self.floatingPlaceholderScale.floatValue) +
                     MDCTextInputControllerBaseDefaultPadding;
  }

  CGFloat scale = UIScreen.mainScreen.scale;
  CGFloat leadingOffset =
      MDCCeil(self.textInput.leadingUnderlineLabel.font.lineHeight * scale) / scale;
  CGFloat calculatedNumberOfLinesForLeadingLabel = [MDCTextInputControllerBase
      calculatedNumberOfLinesForLeadingLabel:self.textInput.leadingUnderlineLabel
                          givenTrailingLabel:self.textInput.trailingUnderlineLabel
                                      insets:defaultInsets
                                   widthHint:widthHint];
  leadingOffset = MAX(leadingOffset, calculatedNumberOfLinesForLeadingLabel * leadingOffset);
  CGFloat trailingOffset =
      MDCCeil(self.textInput.trailingUnderlineLabel.font.lineHeight * scale) / scale;

  // The amount of space underneath the underline is variable. It could just be
  // MDCTextInputControllerBaseDefaultPadding or the biggest estimated underlineLabel height +
  // MDCTextInputControllerBaseDefaultPadding. It's also dependent on the .textInsetsMode.
  CGFloat underlineOffset = 0;
  switch (self.textInput.textInsetsMode) {
    case MDCTextInputTextInsetsModeAlways:
      underlineOffset +=
          MAX(leadingOffset, trailingOffset) + MDCTextInputControllerBaseDefaultPadding;
      break;
    case MDCTextInputTextInsetsModeIfContent: {
      // contentConditionalOffset will have the estimated text height for the largest underline
      // label that also has text.
      CGFloat contentConditionalOffset = 0;
      if (self.textInput.leadingUnderlineLabel.text.length) {
        contentConditionalOffset = leadingOffset;
      }
      if (self.textInput.trailingUnderlineLabel.text.length) {
        contentConditionalOffset = MAX(contentConditionalOffset, trailingOffset);
      }

      if (!MDCCGFloatEqual(contentConditionalOffset, 0)) {
        underlineOffset += contentConditionalOffset + MDCTextInputControllerBaseDefaultPadding;
      }
    } break;
    case MDCTextInputTextInsetsModeNever:
      break;
  }

  // .bottom = underlineOffset + MDCTextInputControllerBaseDefaultPadding
  // Legacy default has an additional padding here but this version does not.
  textInsets.bottom = underlineOffset + MDCTextInputControllerBaseDefaultPadding;
  return textInsets;
}

- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets {
  return [self textInsets:defaultInsets withSizeThatFitsWidthHint:0];
}

// Part of the counting lines logic was sourced from
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html.
+ (NSUInteger)calculatedNumberOfLinesForLeadingLabel:(UILabel *)label
                                  givenTrailingLabel:(UILabel *)trailingLabel
                                              insets:(UIEdgeInsets)insets
                                           widthHint:(CGFloat)widthHint {
  if (!label.text || label.numberOfLines == 1) {
    return 1;
  }
  if (widthHint <= 0 && CGRectGetWidth(label.bounds) <= 0) {
    return 1;
  }

  CGFloat deductedWidthForLeadingLabel = 0;
  // Take into account the width the trailingLabel takes up when calculating the available width.
  if (trailingLabel && trailingLabel.text.length > 0) {
    deductedWidthForLeadingLabel =
        [trailingLabel.text boundingRectWithSize:trailingLabel.frame.size
                                         options:NSStringDrawingUsesLineFragmentOrigin
                                      attributes:@{NSFontAttributeName : trailingLabel.font}
                                         context:nil]
            .size.width;
  }
  // Also take into account the left and right padding of the label when calculating the available
  // width.
  deductedWidthForLeadingLabel += insets.left + insets.right;
  NSTextStorage *textStorage =
      [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
  NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
  [textStorage addLayoutManager:layoutManager];
  CGFloat labelWidth = CGRectGetWidth(label.bounds);
  // Also take int
  CGFloat calculatedWidth = labelWidth > 0 ? labelWidth : widthHint - deductedWidthForLeadingLabel;
  NSTextContainer *textContainer =
      [[NSTextContainer alloc] initWithSize:CGSizeMake(calculatedWidth, CGFLOAT_MAX)];
  textContainer.maximumNumberOfLines = label.numberOfLines;
  textContainer.lineFragmentPadding = 0;
  textContainer.lineBreakMode = label.lineBreakMode;
  [layoutManager addTextContainer:textContainer];
  NSUInteger numberOfLines, index, numberOfGlyphs = [layoutManager numberOfGlyphs];
  NSRange lineRange;
  for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++) {
    (void)[layoutManager lineFragmentRectForGlyphAtIndex:index effectiveRange:&lineRange];
    index = NSMaxRange(lineRange);
  }

  return numberOfLines;
}

- (void)textInputDidLayoutSubviews {
  [self updateBorder];

  if ([self needsUpdatePlaceholderAnimationConstraintsToUp]) {
    [self updatePlaceholderAnimationConstraints:YES];
  }
}

- (void)textInputDidUpdateConstraints {
  if (self.isFloatingEnabled && _textInput.hasTextContent > 0) {
    [self updatePlaceholderAnimationConstraints:YES];
  }
}

#pragma mark - UITextField & UITextView Notification Observation

- (void)textInputDidBeginEditing:(__unused NSNotification *)note {
  [CATransaction begin];
  [CATransaction
      setAnimationDuration:MDCTextInputControllerBaseDefaultFloatingPlaceholderUpAnimationDuration];
  [CATransaction
      setAnimationTimingFunction:[CAMediaTimingFunction
                                     mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]];

  [self updateLayout];

  if (self.isFloatingEnabled && !self.textInput.hasTextContent) {
    [self movePlaceholderToUp:YES];
  }
  [CATransaction commit];

  if (self.characterCountMax > 0) {
    NSString *announcementString;
    if (!announcementString.length) {
      announcementString = [NSString
          stringWithFormat:@"%lu character limit.", (unsigned long)self.characterCountMax];
    }

    // Simply sending a layout change notification does not seem to
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
  }
}

- (void)textInputDidToggleEnabled:(NSNotification *)notification {
  if (notification.object != self.textInput) {
    return;
  }
  [self updateLayout];
}

- (void)textInputDidChange:(NSNotification *)note {
  if ([note.name isEqualToString:MDCTextFieldTextDidSetTextNotification]) {
    [CATransaction begin];
    [CATransaction setAnimationDuration:0];
    [self updateLayout];
    [self forceUpdatePlaceholderY];
    [CATransaction commit];
  } else {
    [self updateLayout];
  }

  // Accessibility
  if (self.textInput.isEditing && self.characterCountMax > 0) {
    NSUInteger charactersForTextInput =
        [self.characterCounter characterCountForTextInput:self.textInput];
    NSString *announcementString;
    if (!announcementString.length) {
      announcementString = [NSString stringWithFormat:@"%lu of %lu characters",
                                                      (unsigned long)charactersForTextInput,
                                                      (unsigned long)self.characterCountMax];
    }

    // Simply sending a layout change notification does not seem to
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString);
  }
}

- (void)textInputDidEndEditing:(__unused NSNotification *)note {
  [CATransaction begin];
  [CATransaction setAnimationDuration:
                     MDCTextInputControllerBaseDefaultFloatingPlaceholderDownAnimationDuration];
  [CATransaction
      setAnimationTimingFunction:[CAMediaTimingFunction
                                     mdc_functionWithType:MDCAnimationTimingFunctionEaseInOut]];

  [self updateLayout];

  if (self.isFloatingEnabled && !self.textInput.hasTextContent) {
    [self movePlaceholderToUp:NO];
  }
  [CATransaction commit];
}

#pragma mark - Public API

- (void)setErrorText:(NSString *)errorText
    errorAccessibilityValue:(NSString *)errorAccessibilityValue {
  // Turn on error:
  //
  // Here the 'magic' logic happens for error text.
  // When the user sets error text, we save the current state of their underline, leading text,
  // trailing text, and placeholder text for both content and color.
  if (errorText && !self.isDisplayingErrorText) {
    // If we are not in error, but will be, we need to save the existing state.
    self.previousLeadingText = self.textInput.leadingUnderlineLabel.text
                                   ? self.textInput.leadingUnderlineLabel.text.copy
                                   : @"";

    self.textInput.leadingUnderlineLabel.text = errorText;

    [self updatePlaceholder];
  }

  // Change error:
  if (errorText && self.isDisplayingErrorText) {
    self.textInput.leadingUnderlineLabel.text = errorText;
  }

  // Turn off error:
  //
  // If error text is unset (nil) we reset to previous values.
  if (!errorText) {
    // If there is a saved state, use it.
    if (self.previousLeadingText) {
      self.textInput.leadingUnderlineLabel.text = self.previousLeadingText;
    }

    // Clear out saved state.
    self.previousLeadingText = nil;
  }

  self.errorText = errorText;
  self.errorAccessibilityValue = errorAccessibilityValue;

  [self updateLayout];

  // Accessibility
  // TODO: (larche) Localize
  if (errorText) {
    NSString *announcementString = errorAccessibilityValue;
    if (!announcementString.length) {
      announcementString =
          errorText.length > 0 ? [NSString stringWithFormat:@"Error: %@", errorText] : @"Error.";
    }
    self.textInput.leadingUnderlineLabel.accessibilityLabel = announcementString;

    dispatch_after(
        dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDefaultErrorAnnouncementDelay * NSEC_PER_SEC)),
        dispatch_get_main_queue(), ^{
          UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
                                          announcementString);
        });
  }
  // Restore the helper text accessibilityLabel.
  else {
    if ([self.textInput.leadingUnderlineLabel.text isEqualToString:self.helperText]) {
      self.textInput.leadingUnderlineLabel.accessibilityLabel = self.helperAccessibilityLabel;
    } else {
      self.textInput.leadingUnderlineLabel.accessibilityLabel = nil;
    }
  }
}

- (void)setHelperText:(NSString *)helperText
    helperAccessibilityLabel:(NSString *)helperAccessibilityLabel {
  self.helperText = helperText;
  self.helperAccessibilityLabel = helperAccessibilityLabel;
}

#pragma mark - Accessibility

- (BOOL)mdc_adjustsFontForContentSizeCategory {
  return _mdc_adjustsFontForContentSizeCategory;
}

- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
  _mdc_adjustsFontForContentSizeCategory = adjusts;

  [self updateLayout];

  if (_mdc_adjustsFontForContentSizeCategory) {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(contentSizeCategoryDidChange:)
                                                 name:UIContentSizeCategoryDidChangeNotification
                                               object:nil];
  } else {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:UIContentSizeCategoryDidChangeNotification
                                                  object:nil];
  }
}

+ (BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
  return _mdc_adjustsFontForContentSizeCategoryDefault;
}

+ (void)setMdc_adjustsFontForContentSizeCategoryDefault:
    (BOOL)mdc_adjustsFontForContentSizeCategoryDefault {
  _mdc_adjustsFontForContentSizeCategoryDefault = mdc_adjustsFontForContentSizeCategoryDefault;
}

- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
  [self updateLayout];
}

@end
